P01-1. 서비스 메시의 탄생과 Envoy xDS - 이론 개념

Part 01 (이론): 서비스 메시(Service Mesh)의 탄생과 Envoy xDS 해부

핵심 학습 목표

  1. 서비스가 여러 개로 쪼개진 환경(MSA)에서 왜 동서 트래픽(East-West Traffic) 통제가 가장 큰 골칫거리가 되었는지 설명할 수 있다.
  2. 프록시(Proxy) 와 사이드카(Sidecar) 패턴이 무엇인지, 왜 내 앱 옆에 비서 컨테이너를 붙이는지 구조적으로 이해한다.
  3. 쿠버네티스 기본 Service(kube-proxy, L4) 가 왜 HTTP Keep-Alive 와 gRPC 앞에서 한쪽 파드로만 요청을 몰아주는지(연결 고착화), 그리고 Envoy(L7) 는 이를 어떻게 해결하는지 완벽히 이해한다.
  4. 서비스 메시의 두 기둥인 컨트롤 플레인(istiod) 과 데이터 플레인(Envoy) 의 역할을 구분할 수 있다.
  5. Envoy 프록시가 재시작 없이 실시간으로 설정을 바꾸는 5가지 핵심 규격(xDS: LDS → RDS → CDS → EDS + SDS)의 흐름을 그림으로 설명할 수 있다.

선수 지식 요약 (기초 쿠버네티스 오브젝트)

  • Pod (파드): 쿠버네티스에서 컨테이너를 담는 가장 작은 실행 단위. 파드 하나는 고유한 IP 주소(10.244.x.x) 하나를 가집니다.
  • Deployment (디플로이먼트): 똑같은 파드를 여러 개(예: 2개, 3개) 복제해서 항상 살아 있게 유지해 주는 컨트롤러입니다.
  • Service (서비스): 죽고 살아날 때마다 IP가 바뀌는 파드들 앞에 세워 두는 고정된 대표 주소(이름 + ClusterIP) 입니다.
  • Gateway API (게이트웨이): 클러스터 바깥의 사용자가 인터넷을 통해 클러스터 안으로 들어올 때 거치는 클러스터 정문입니다.


1. 왜 서비스 메시(Service Mesh)인가? — 한 건물에서 여러 건물로 쪼개진 날

1-1. 백화점 한 건물(모놀리식) vs 캠퍼스의 여러 건물(MSA)

옛날에는 쇼핑몰을 만들 때 회원·상품·주문·결제·배송 기능을 전부 단 하나의 거대한 프로그램 안에 넣어서 만들었습니다. 이것을 모놀리식(Monolithic, 한 덩어리) 아키텍처라고 부릅니다.

모놀리식에서는 '주문' 기능이 '결제' 기능을 부를 때 어떻게 했을까요? 그냥 같은 프로그램 메모리 안에서 함수 하나(payment.pay()) 를 부르면 끝났습니다. 한 건물 같은 방에 앉아 있는 옆 자리 동료에게 말로 물어보는 것과 같아서, 0.001초도 안 걸리고 중간에 전화선이 끊길 일도 없었습니다.

하지만 회사가 커지고 개발자가 100명이 넘어가자, 한 덩어리 프로그램은 건드리기만 하면 전체가 다 멈추는 폭탄이 되었습니다. 그래서 오늘날에는 기능별로 프로그램을 잘게 쪼개서 각자 별도의 파드(Pod)로 띄우는 마이크로서비스(MSA, Microservices Architecture) 방식을 씁니다.

flowchart LR
    subgraph MONO["과거: 모놀리식 (한 건물 안)"]
        direction TB
        M1["주문 + 결제 + 재고 + 배송
(하나의 거대한 프로세스)"] M2["메모리 함수 호출: pay()
⚡ 0.001ms / 끊김 없음"] M1 --- M2 end subgraph MSA["현재: 마이크로서비스 (여러 파드 간 네트워크 통신)"] direction LR P1["주문 파드
(Java)"] -- "① HTTP/gRPC 호출" --> P2["결제 파드
(Go)"] P2 -- "② HTTP/gRPC 호출" --> P3["포인트 파드
(Python)"] P2 -- "③ TCP 쿼리" --> DB["PostgreSQL DB"] end

문제는 이제부터입니다. 한 방에 있던 동료들이 각자 다른 건물(다른 파드, 다른 서버)로 흩어졌기 때문에, 이제 주문 파드가 결제 파드를 부르려면 무조건 네트워크(사내 전화선) 를 타고 가야 합니다!

네트워크를 타는 순간, 예전에는 상상도 못 했던 질문들이 쏟아집니다.


1-2. 남북 트래픽(North-South) vs 동서 트래픽(East-West)

방금 본 문제를 네트워크 아키텍처에서는 방향으로 나누어 부릅니다. 왜 하필 이름을 남북(North-South) 과 동서(East-West) 라고 붙였을까요?

우리가 구성도를 그릴 때 보통 맨 위(북쪽, North) 에 바깥 사용자를 그리고, 맨 아래(남쪽, South) 에 서버들을 그리기 때문입니다!

flowchart TB
    USER["👤 외부 사용자 (북쪽, North)
스마트폰 · 웹 브라우저"] GW["🚪 Gateway API / LoadBalancer
(클러스터 정문 검문소)"] USER -- "⬇️ 남북 트래픽 (North-South)
외부 ↔ 내부 출입" --> GW subgraph CLUSTER["Kubernetes 클러스터 내부 (남쪽, South)"] direction LR A["주문 파드
(order-api)"] B["결제 파드
(payment-api)"] C["재고 파드
(stock-api)"] A -- "➡️ 동서 트래픽 (East-West)" --> B B -- "➡️ 동서 트래픽 (East-West)" --> C end GW --> A
구분 남북 트래픽 (North-South Traffic) 동서 트래픽 (East-West Traffic)
방향 (구성도 기준) 위 ↔ 아래 (클러스터 바깥 ↔ 클러스터 안) 왼쪽 ↔ 오른쪽 (클러스터 안의 파드 ↔ 옆 파드)
비유 건물 밖 손님이 정문 로비를 통과해 들어오는 출입 건물 안에서 주문팀 직원이 결제팀·재고팀에 돌리는 내선 전화
누가 담당하나요? Gateway API (예: Envoy Gateway), 외부 로드밸런서 서비스 메시 (Service Mesh) ← 우리가 배울 주인공!
실제 통신량 비중 손님이 버튼 1번 누름 (약 10~20%) 그 1번을 처리하려고 내부 파드끼리 5~10번 연쇄 호출 (80~90%)

기초 과정에서 배운 Gateway API 는 훌륭한 건물 정문 경비원입니다. 하지만 손님이 정문을 한 번 통과하고 난 뒤, 건물 안에서 주문 파드가 결제 파드를 부르고 결제 파드가 재고 파드를 부르는 동서 트래픽(내선 통화) 은 정문 경비원이 전혀 볼 수도 없고 통제할 수도 없습니다.


1-3.1세대 해결책의 실패: "앱 코드 안에 직접 짜 넣자!" (폴리글랏 지옥)

처음(2010년대 초중반, Netflix 등)에는 개발자들이 이렇게 생각했습니다.

"그럼 주문 파드 소스 코드 안에다가 결제 파드 부를 때 3초 넘으면 끊기(Timeout), 실패하면 2번 다시 해보기(Retry), 계속 실패하면 아예 차단하기(Circuit Breaker) 기능을 라이브러리로 다 짜 넣으면 되겠네!"

이 방식은 회사가 딱 한 가지 프로그래밍 언어(예: Java) 만 쓸 때는 그럭저럭 돌아갔습니다. 하지만 곧 한계에 부딪혔습니다.

  1. 폴리글랏(Polyglot, 다중 언어) 환경의 등장: 주문팀은 Java(Spring), AI 추천팀은 Python(FastAPI), 결제팀은 Go, 웹 화면팀은 Node.js(TypeScript) 를 씁니다. 똑같은 타임아웃·서킷브레이커·암호화 로직을 4가지 언어로 각각 개발하고 유지보수하는 것은 불가능에 가까웠습니다.
  2. 비즈니스 로직과 인프라 코드의 뒤섞임: 개발자는 "쿠폰 할인율 계산" 같은 비즈니스 코드만 짜고 싶은데, 코드의 절반이 네트워크 통신 예외 처리와 인증서 갱신 코드로 뒤덮였습니다.
  3. 설정 하나 바꾸려면 전체 재배포: "결제 타임아웃을 3초에서 5초로 늘리자"는 사소한 변경 하나를 하려고 전사 마이크로서비스 50개를 다시 빌드해서 재배포해야 했습니다.
용어 정리

  • 모놀리식 (Monolithic): 모든 기능이 하나의 큰 프로그램(프로세스) 안에 합쳐져 있는 전통적인 소프트웨어 구조입니다.
  • 마이크로서비스 (MSA, Microservices Architecture): 기능을 작은 단위로 쪼개어 각각 독립된 프로그램(파드)으로 띄우고, 서로 네트워크로 대화하게 만든 구조입니다.
  • 남북 트래픽 (North-South Traffic): 클러스터 외부(고객)에서 내부로 들어오거나 나가는 수직 방향 트래픽입니다.
  • 동서 트래픽 (East-West Traffic): 클러스터 내부에 있는 파드(서비스)들끼리 서로 주고받는 수평 방향 트래픽입니다.
  • 폴리글랏 (Polyglot): 원래는 "여러 나라 말을 할 줄 아는 사람"이라는 뜻이며, IT에서는 한 시스템 안에서 Java, Python, Go, Node.js 등 여러 프로그래밍 언어를 섞어 쓰는 환경을 뜻합니다.
  • 타임아웃 (Timeout): 상대방 서버에 요청을 보낸 뒤, 정해진 시간(예: 3초) 안에 답이 안 오면 무한정 기다리지 않고 연결을 끊는 안전장치입니다.
  • 재시도 (Retry): 일시적인 네트워크 흔들림으로 요청이 실패했을 때, 자동으로 한두 번 다시 요청을 보내 보는 기능입니다.
  • 서킷 브레이커 (Circuit Breaker, 회로 차단기): 집에서 전기가 과부하 걸리면 두꺼비집(차단기)이 내려가서 화재를 막듯, 고장 난 상대방 파드로는 아예 요청을 보내지 않고 즉시 차단해 내 파드까지 같이 죽는 연쇄 장애를 막는 기술입니다.


2. 프록시(Proxy)와 사이드카(Sidecar) 패턴 — "운전은 개발자가, 무전과 검문은 옆자리 비서가"

이 지옥 같은 문제를 해결하기 위해 엔지니어들이 떠올린 아이디어는 아주 단순하고 명쾌했습니다.

"통신·보안·재시도·관측 같은 골치 아픈 네트워크 일은 앱 코드 안에서 완전히 빼버리자! 대신 그 일만 전문으로 하는 '전담 비서 프로그램(Proxy)'을 만들어서 모든 파드 옆에 한 명씩 붙여 주자!"

이 아이디어를 이해하려면 먼저 프록시(Proxy) 와 사이드카(Sidecar) 라는 두 단어를 정확히 알아야 합니다.


2-1. 프록시(Proxy)란 무엇인가요?

프록시(Proxy) 는 영어로 "대리인, 대신 일해 주는 비서" 라는 뜻입니다. 컴퓨터 네트워크에서는 누군가와 직접 통신하지 않고, 중간에 서서 대신 요청을 보내주거나 대신 요청을 받아주는 프로그램을 모두 프록시라고 부릅니다.

프록시는 누구 편에 서서 일하느냐에 따라 두 가지로 나뉩니다.

flowchart LR
    subgraph CLIENT["보내는 쪽 (주문 파드)"]
        A["주문 앱"] --> FP["① 포워드 프록시
(송신 전담 비서)"] end subgraph SERVER["받는 쪽 (결제 파드)"] RP["② 리버스 프록시
(수신 전담 비서)"] --> B["결제 앱"] end FP -- "암호화 · 재시도 · 길 찾기" --> RP
구분 ① 포워드 프록시 (Forward Proxy) ② 리버스 프록시 (Reverse Proxy)
누구 앞에 서 있나요? 요청을 보내는 사람(클라이언트) 바로 앞 요청을 받는 서버 바로 앞
비유 사장님(내 앱)이 "결제팀에 이거 전해줘"라고 시키면, 대신 전화를 걸고 안 받으면 재시도까지 해 주는 발신 비서 외부에서 결제팀으로 걸려 온 전화를 먼저 받아서 신분증을 검사하고 안쪽으로 연결해 주는 수신 비서
하는 일 목적지 서버 위치 찾기(로드밸런싱), 타임아웃, 재시도, 나가는 트래픽 암호화 들어오는 트래픽 복호화, 권한 검사(인가), 내 서버 보호

2-2. 사이드카(Sidecar) 패턴과 서비스 메시(Service Mesh)의 탄생

그렇다면 이 유능한 비서(프록시)를 어디에 두어야 할까요? 여기서 쿠버네티스 Pod(파드) 의 특징이 빛을 발합니다.

기초 과정에서 배웠듯, 쿠버네티스의 Pod 하나 안에는 컨테이너를 여러 개 넣을 수 있습니다. 그리고 같은 Pod 안에 들어간 컨테이너들은 마치 한 몸처럼 똑같은 IP 주소와 로컬 네트워크(localhost)를 공유합니다!

마치 오토바이(본체) 옆에 보조 좌석(Sidecar)을 달아 놓은 것과 똑같다고 해서, 원래의 비즈니스 앱 컨테이너 옆에 보조 컨테이너를 나란히 끼워 넣는 설계 방식을 사이드카 패턴(Sidecar Pattern) 이라고 부릅니다.

flowchart TB
    subgraph POD_A["주문 Pod (IP: 10.244.1.5)"]
        direction TB
        APP_A["🛵 비즈니스 컨테이너 (주문 앱)
오직 '주문 로직'만 작성됨 (Python/Java)"] ENVOY_A["🛡️ 사이드카 컨테이너 (Envoy 프록시)
포워드 + 리버스 프록시 역할 동시 수행"] APP_A -- "localhost 통신" --- ENVOY_A end subgraph POD_B["결제 Pod (IP: 10.244.2.8)"] direction TB ENVOY_B["🛡️ 사이드카 컨테이너 (Envoy 프록시)
신분증 검사 · 암호 해독 · 메트릭 기록"] APP_B["🛵 비즈니스 컨테이너 (결제 앱)
오직 '결제 로직'만 작성됨 (Go)"] ENVOY_B -- "localhost 통신" --- APP_B end ENVOY_A -- "🔒 암호화된 동서 트래픽 (mTLS)
L7 로드밸런싱 · 타임아웃 · 재시도" --> ENVOY_B
  1. 주문 앱(오토바이 운전자) 은 복잡한 네트워크 기술을 전혀 모릅니다. 그냥 평소처럼 http://payment-api/pay 로 평범한 요청을 던집니다.
  2. 그러면 같은 파드 옆자리에 타고 있던 주문 파드의 Envoy 사이드카(포워드 프록시 비서) 가 이 요청을 낚아챕니다.
  3. 주문 쪽 비서는 가장 한가한 결제 파드의 IP를 골라서 암호화(mTLS)한 뒤, 결제 파드의 Envoy 사이드카(리버스 프록시 비서) 에게 전달합니다.
  4. 결제 쪽 비서는 암호를 풀고 신분증을 검사한 뒤, 안전하다고 확인되면 자기 파드 안의 결제 앱에게 조용히 넘겨줍니다.

클러스터 안의 파드 100개에 모두 이 사이드카 비서들을 한 명씩 붙여 놓고 나니, 비서들끼리 서로 거미줄·그물망(Mesh)처럼 촘촘하게 연결된 거대한 통신망이 만들어졌습니다. 그래서 이 구조를 "서비스 메시(Service Mesh)" 라고 부릅니다!

용어 정리

  • 프록시 (Proxy): 클라이언트와 서버 사이에서 통신을 대신 중계해 주는 대리인 프로그램입니다.
  • 포워드 프록시 (Forward Proxy): 보내는 쪽(클라이언트) 앞에 서서 외부로 나가는 요청을 대신 처리해 주는 프록시입니다.
  • 리버스 프록시 (Reverse Proxy): 받는 쪽(서버) 앞에 서서 안으로 들어오는 요청을 먼저 받아 검사하고 전달해 주는 프록시입니다.
  • 사이드카 (Sidecar): 오토바이 옆 보조 좌석이라는 뜻으로, 쿠버네티스에서는 원래 앱 컨테이너와 같은 Pod 안에 함께 들어앉아 보조 역할(네트워크, 로그 수집 등)을 해 주는 컨테이너를 뜻합니다. 사이드카가 붙은 파드는 kubectl get pods를 치면 READY 칸이 1/1이 아니라 2/2(내 앱 1개 + 사이드카 1개)로 표시됩니다.
  • 인프로세스 (In-Process) vs 아웃오브프로세스 (Out-of-Process): 옛날처럼 앱 코드 안에 라이브러리를 직접 넣어서 한 프로세스로 돌리는 것을 In-Process, 사이드카처럼 내 앱 바깥의 별도 프로세스(별도 컨테이너)로 분리해 돌리는 것을 Out-of-Process 방식이라고 합니다.
  • 서비스 메시 (Service Mesh): 마이크로서비스(파드)들 사이의 동서 트래픽을 애플리케이션 코드 수정 없이 프록시들의 그물망(Mesh)으로 안전하고 똑똑하게 제어·관측하는 인프라 계층입니다.


3. 초보자가 가장 많이 묻는 핵심 질문: "쿠버네티스에 이미 Service(kube-proxy)가 있는데 왜 프록시를 또 쓰나요?"

여기서 아주 날카롭고 당연한 질문이 하나 나옵니다.

"잠깐만요! 앞선 쿠버네티스 기초 스터디 8주차에서 Service 를 만들면 각 노드의 kube-proxy 가 알아서 파드 여러 개로 요청을 나눠 준다(로드밸런싱)고 배웠잖아요? 그런데 왜 굳이 무겁게 Envoy 프록시를 또 띄워야 하나요?"

이 질문에 정확히 대답할 수 있다면 이미 주니어 수준을 넘어선 것입니다. 비밀은 네트워크의 L4(4계층) 와 L7(7계층) 의 결정적인 차이에 있습니다.


3-1. 택배로 이해하는 L4(TCP) vs L7(HTTP/gRPC)

컴퓨터 네트워크는 하는 일에 따라 층(Layer)이 나뉘어 있는데, 실무에서는 딱 두 층만 확실히 구분하면 됩니다.

구분 L4 — 전송 계층 (Transport Layer) L7 — 응용 계층 (Application Layer)
대표 프로토콜 TCP, UDP HTTP/1.1, HTTP/2, gRPC, WebSocket
비유 택배 트럭(연결 통로) 과 상자 겉면의 주소(IP:포트) 상자를 열었을 때 안에 들어 있는 편지 내용(GET /pay, 헤더, 상태코드 200/500)
볼 수 있는 정보 출발지 IP:포트, 목적지 IP:포트, 전화 연결 시작(SYN)/종료(FIN) URL 경로(/orders), 도메인(Host), 쿠키/헤더, 응답 코드(500 에러), 요청 1건당 걸린 시간
쿠버네티스 대표 주자 kube-proxy (기본 Service),2세대 ztunnel Envoy 프록시 (Istio Sidecar, Gateway, 2세대 Waypoint)

3-2. 전화 걸기(TCP Handshake)와 끊지 않고 말하기(HTTP Keep-Alive)

두 파드가 HTTP로 통신할 때 실제로는 어떤 일이 벌어질까요?

  1. 먼저 전화를 겁니다 (TCP 3-Way Handshake):
    • 편지(HTTP 요청)를 보내려면 먼저 두 컴퓨터 사이에 TCP 연결(전화선) 을 개통해야 합니다.
    • "여보세요?"(SYN) → "네 잘 들립니다"(SYN+ACK) → "네, 이제 용건 말할게요"(ACK) 이렇게 세 번 신호를 주고받아서 전화선을 여는 과정을 TCP 3-Way Handshake 라고 부릅니다.
  2. 전화를 끊지 않고 편지를 계속 보냅니다 (HTTP Keep-Alive):
    • 만약 주문 파드가 결제 파드에 1초에 100번 요청을 보내는데, 매번 전화를 끊었다가 다시 걸면(Handshake 100번 반복) 전화 거는 시간 때문에 속도가 엄청나게 느려집니다.
    • 그래서 현대의 모든 프로그램(HTTP/1.1의 기본값, Python requests.Session, Java HttpClient, Node.js fetch, 그리고 gRPC)은 맨 처음에 한 번만 전화선(TCP 연결)을 열어 두고, 그 전화선을 끊지 않은 채로 수백·수천 개의 HTTP 요청을 연속해서 주고받습니다.
    • 이렇게 한 번 맺은 TCP 연결을 끊지 않고 재사용하는 기술을 HTTP Keep-Alive (지속 연결, Persistent Connection) 라고 부릅니다!

3-3. kube-proxy 의 치명적인 함정: L4 연결 고착화 (Connection Pinning)

자, 이제 HTTP Keep-Alive 가 켜진 상태에서 쿠버네티스 기본 kube-proxy(L4) 가 어떻게 동작하는지 보겠습니다.

flowchart LR
    subgraph L4_TRAP["❌ 기본 kube-proxy (L4): 처음 전화 걸 때만 주사위를 굴림!"]
        direction LR
        C1["주문 파드
(Keep-Alive 연결 1개 유지)
요청 ①, ②, ③, ④ 연속 발송"] -- "단 1개의 TCP 전화선
(요청 4개 모두 통과)" --> KP["kube-proxy (L4)
'첫 전화(SYN)가 2번 파드로 연결됐으니
전화 끊길 때까지 무조건 2번으로!'"] KP -. "0건 (놀고 있음)" .-> P1["결제 파드 1
CPU 0% 💤"] KP == "요청 ①, ②, ③, ④ 전부 몰림!" ==> P2["결제 파드 2
CPU 100% 🔥"] end
기본 kube-proxy 만 믿었다가 실무에서 터지는 3대 참사

  1. Keep-Alive / gRPC 편중(Hotspot) 장애: 특히 요즘 많이 쓰는 gRPC 는 내부적으로 HTTP/2 멀티플렉싱(Multiplexing) 이라는 기술을 써서 아예 단 1개의 TCP 연결만 맺고 모든 요청을 동시에 주고받습니다. 그래서 서비스 메시 없이 gRPC를 쓰면 파드를 10개 띄워도 1개만 죽어라 일하고 9개는 노는 현상이 100% 발생합니다.
  2. 오토스케일링(HPA) 무력화: 갑자기 주문이 폭주해서 쿠버네티스가 결제 파드를 2개에서 5개로 늘려 주었습니다(Scale-out). 하지만 이미 1·2번 파드와 전화선(Keep-Alive)을 연결해 둔 클라이언트들은 전화를 끊지 않으니, 새로 뜬 3·4·5번 파드로는 요청이 단 1건도 가지 않고 기존 1·2번 파드가 그대로 과부하로 죽어버립니다!
  3. 속 빈 강정(L7 에러) 감지 불가: 결제 파드 2번이 DB 연결이 끊겨서 모든 요청에 HTTP 500 에러를 뱉고 있습니다. 하지만 파드 프로세스 자체는 살아 있고 TCP 전화선도 안 끊어졌으니, kube-proxy는 2번 파드가 고장 난 줄 전혀 모르고 계속 요청을 보냅니다.


3-4. Envoy 프록시(L7)는 이 문제를 어떻게 해결하나요?

반면 파드 옆에 붙은 Envoy 사이드카 프록시는 L7(HTTP 편지 내용) 까지 다 뜯어볼 줄 아는 똑똑한 비서입니다!

flowchart LR
    subgraph L7_SOLVE["✅ Envoy 프록시 (L7): 전화선은 유지한 채 편지(HTTP 요청)를 한 장씩 나눠 보냄!"]
        direction LR
        C2["주문 파드
(Keep-Alive 연결 유지)
요청 ①, ②, ③, ④ 연속 발송"] -- "TCP 연결 1개" --> ENV["주문 파드의 Envoy (L7)
편지 4장을 한 장씩 뜯어서
번갈아 배달!"] ENV == "요청 ①, ③ 배달" ==> P3["결제 파드 1
(10.244.1.2) ⚖️ 50%"] ENV == "요청 ②, ④ 배달" ==> P4["결제 파드 2
(10.244.2.2) ⚖️ 50%"] end
  1. 주문 앱이 Keep-Alive로 전화선 1개를 유지한 채 요청 ①, ②, ③, ④를 연속으로 보내도, 바로 옆에 있는 Envoy 프록시가 그 전화선 안의 HTTP 요청 4개를 한 장씩 낱개로 분리해 냅니다.
  2. 그리고 Envoy는 결제 파드 1번(10.244.1.2), 결제 파드 2번(10.244.2.2)과 각각 미리 열어 둔 전화선을 통해 요청 ①은 1번 파드로, 요청 ②는 2번 파드로, 요청 ③은 1번 파드로, 요청 ④는 2번 파드로 칼같이 나눠서 보냅니다!
  3. 만약 결제 파드 2번이 HTTP 500 에러를 연속으로 뱉으면? Envoy가 즉시 편지 내용을 보고 "어? 2번 파드가 500 에러를 내네! 30초 동안 2번 파드는 명단에서 빼고 1번 파드로만 보내자!"(서킷 브레이커 / Outlier Detection)라고 알아서 조치합니다.
오늘 1주차 실습의 하이라이트!

잠시 뒤 실습 가이드에서 사이드카를 붙이기 전(kube-proxy) 에 Keep-Alive로 4번 연속 호출했을 때 4번 모두 똑같은 파드로만 몰리는 현상을 직접 재현하고, Istio 사이드카를 붙인 직후 똑같은 명령어를 쳤을 때 두 파드로 번갈아 가며 분산되는 장면을 내 눈으로 직접 확인합니다!

용어 정리

  • L4 (Transport Layer, 전송 계층): IP 주소와 포트 번호(TCP/UDP)만 보고 패킷을 전달하는 네트워크 계층입니다. 연결이 맺어지고 끊어지는 것만 알 뿐, 그 안에 어떤 HTTP 요청이 오가는지는 알지 못합니다.
  • L7 (Application Layer, 응용 계층): HTTP/1.1, HTTP/2, gRPC 처럼 실제 애플리케이션이 주고받는 메시지(URL, 헤더, 메서드, 상태 코드)를 읽고 이해하는 네트워크 계층입니다.
  • TCP 3-Way Handshake: TCP 통신을 시작할 때 양쪽 컴퓨터가 SYN → SYN+ACK → ACK 세 번 신호를 주고받아 연결 통로를 여는 과정입니다.
  • HTTP Keep-Alive (지속 연결): 매번 HTTP 요청을 보낼 때마다 TCP 연결을 새로 맺고 끊는 대신, 한 번 맺은 TCP 연결을 계속 살려 두고 여러 개의 HTTP 요청을 연달아 주고받는 기술입니다.
  • HTTP/2 멀티플렉싱 (Multiplexing)과 gRPC: 하나의 TCP 연결 위에서 여러 개의 요청과 응답을 순서 기다림 없이 동시에 주고받는 기술입니다. 고성능 통신 규격인 gRPC가 이 방식을 사용하는데, 연결을 딱 1개만 유지하므로 L4 로드밸런서(kube-proxy)만 쓰면 부하 분산이 전혀 되지 않습니다.
  • conntrack (Connection Tracking): 리눅스 커널 안에 있는 '현재 통화 중인 연결 장부'입니다. kube-proxy는 첫 패킷이 올 때만 목적지 파드를 고르고 이 장부에 적어 둔 뒤, 연결이 끝날 때까지 장부에 적힌 똑같은 파드로만 패킷을 보냅니다.
  • 커넥션 피닝 (Connection Pinning, 연결 고착화): Keep-Alive 등으로 인해 한 번 맺어진 연결의 모든 후속 요청이 특정 서버(파드) 한 곳으로만 계속 쏠리는 현상입니다.


4. 서비스 메시의 두 기둥: 컨트롤 플레인(istiod)과 데이터 플레인(Envoy)

우리가 이번 스터디에서 사용할 서비스 메시의 표준 오픈소스는 Istio(이스티오) 이며, Istio 안에서 실제로 패킷을 나르는 프록시 엔진은 Envoy(엔보이) 입니다.

쿠버네티스 클러스터가 Control Plane(관리 본부) 과 Worker Node(일꾼 서버) 로 나뉘어 있듯, 서비스 메시 아키텍처도 정확히 두 개의 층(Plane) 으로 나뉩니다.

flowchart TB
    ADMIN["👩‍💻 운영자 / 아키텍트
kubectl apply (라우팅 · 보안 정책)"] --> KAPI["kube-apiserver
(쿠버네티스 API 창구)"] subgraph CP["🧠 컨트롤 플레인 (Control Plane) — 통제 본부"] ISTIOD["istiod (Istio Daemon)
① K8s Service/Pod 변화 실시간 감시
② YAML 정책을 Envoy 전용 설정(xDS)으로 번역
③ 파드마다 전자 신분증(SPIFFE 인증서) 발급"] end KAPI -- "Service / EndpointSlice / 정책 알림" --> ISTIOD subgraph DP["💪 데이터 플레인 (Data Plane) — 현장 요원들"] direction LR subgraph POD1["주문 파드 (study-client)"] A1["주문 앱"] --- E1["Envoy 사이드카
(istio-proxy)"] end subgraph POD2["결제 파드 1 (order-api-1)"] E2["Envoy 사이드카
(istio-proxy)"] --- A2["결제 앱 1"] end subgraph POD3["결제 파드 2 (order-api-2)"] E3["Envoy 사이드카
(istio-proxy)"] --- A3["결제 앱 2"] end E1 == "실제 고객 트래픽 (HTTP / mTLS)" ==> E2 E1 == "실제 고객 트래픽 (HTTP / mTLS)" ==> E3 end ISTIOD -. "⚡ xDS 실시간 작전 지시 (gRPC 스트리밍)
(고객 트래픽은 여기를 지나가지 않음!)" .-> E1 ISTIOD -. "⚡ xDS 실시간 작전 지시" .-> E2 ISTIOD -. "⚡ xDS 실시간 작전 지시" .-> E3

4-1. 데이터 플레인 (Data Plane) = 현장에서 뛰는 요원 (Envoy)

4-2. 컨트롤 플레인 (Control Plane) = 본부 상황실 (istiod)

면접과 실무에서 정말 자주 나오는 질문!

Q. 만약 통제 본부인 istiod 파드가 갑자기 죽거나 재시작되면, 지금 오가고 있는 고객들의 주문·결제 트래픽도 전부 멈추나요?
A. 아니요, 단 1건도 끊기지 않고 정상 동작합니다!
위 그림에서 보듯, 실제 고객 트래픽(굵은 실선)은 오직 현장의 Envoy 프록시(데이터 플레인) 들끼리만 주고받으며 istiod를 전혀 거치지 않습니다. istiod가 잠깐 죽어도 각 Envoy는 이미 자기 메모리에 받아 둔 최신 지도와 신분증을 가지고 계속 통신을 처리합니다. (다만 istiod가 죽어 있는 동안 새로 추가된 파드 IP나 새로 바꾼 라우팅 설정만 istiod가 다시 살아날 때까지 업데이트가 잠시 미뤄질 뿐입니다.)

용어 정리

  • 데이터 플레인 (Data Plane): 실제 네트워크 패킷(사용자 요청과 응답)이 지나가며 처리되는 현장 경로입니다. Istio에서는 Envoy 프록시(및 ztunnel)가 담당합니다.
  • 컨트롤 플레인 (Control Plane): 패킷을 직접 나르지 않고, 데이터 플레인의 프록시들이 어떻게 동작해야 할지 설정·경로·인증서를 내려주는 두뇌(통제 본부)입니다. Istio에서는 istiod 가 담당합니다.
  • Envoy Proxy (엔보이 프록시): 차량 공유 기업 Lyft가 C++로 개발해 CNCF에 기증한 초고성능 L4/L7 오픈소스 프록시입니다. 오늘날 Istio, Envoy Gateway, AWS App Mesh, Cilium 등 거의 모든 클라우드 네이티브 네트워크 도구의 심장으로 쓰입니다.
  • istiod (Pilot + Citadel + Galley): Istio의 컨트롤 플레인 데몬입니다. 예전 Istio(1.4 버전 이전)에서는 길 안내 담당(Pilot), 인증서 발급 담당(Citadel), 설정 검증 담당(Galley)이 별개의 프로그램으로 나뉘어 있었으나, 지금은 istiod 단 하나의 프로그램 안에 세 가지 기능이 모두 합쳐졌습니다. 예전 문서를 읽을 때 Pilot이라는 단어가 나오면 "아, 지금의 istiod 안에 있는 라우팅 모듈이구나!"라고 이해하면 됩니다.


5. Envoy 프록시의 심장: xDS 동적 설정 프로토콜 5단계 해부

자, 이제 1주차의 가장 중요한 하이라이트이자 실습에서 직접 뜯어볼 xDS 입니다!

5-1. 왜 이름이 xDS 인가요? 왜 설정 파일을 쓰지 않을까요?

전통적인 웹 서버나 프록시(예: NGINX, HAProxy)는 주소를 바꾸려면 사람이 nginx.conf 같은 설정 파일을 열어서 고친 다음 프로그램을 재시작(reload) 해야 했습니다.

하지만 쿠버네티스에서는 파드가 오토스케일링이나 롤링 업데이트로 인해 1분에도 수십 개씩 새로 생기고 사라지며 IP가 계속 바뀝니다. 그때마다 설정 파일을 다시 쓰고 프록시를 재시작할 수는 없습니다.

그래서 Envoy는 설정 파일 대신, 통제 본부(istiod)와 24시간 끊기지 않는 직통 전화(양방향 gRPC 스트리밍) 를 연결해 두고 바뀐 정보만 실시간으로 받아서 메모리에 즉시 끼워 넣습니다. 이때 정보를 종류별로 주고받는 5가지 규격의 이름이 모두 ... Discovery Service (자동 탐색 서비스, 줄여서 DS) 로 끝납니다.

앞글자(L, R, C, E, S)만 바뀌고 뒤가 전부 DS 로 끝나기 때문에, 수학에서 미지수 x를 쓰듯 앞글자를 소문자 x 로 묶어서 xDS 라고 부르는 것입니다!


5-2. 우체국 택배 분류로 이해하는 xDS 5단계 흐름

내 파드(study-client) 안에서 curl http://order-api:80/pay 라고 요청을 보냈을 때, 내 파드 옆에 있는 Envoy 프록시는 LDS → RDS → CDS → EDS (+ SDS) 순서로 지도를 펼쳐 보고 패킷을 배달합니다.

flowchart LR
    REQ["① 내 앱의 요청
GET http://order-api:80/pay"] --> LDS subgraph ENVOY["내 파드 옆의 Envoy 프록시 메모리 (istioctl proxy-config 로 확인 가능)"] direction LR LDS["1️⃣ LDS (Listener)
🚪 접수 창구 열기
'15001번 창구로 접수!'"] RDS["2️⃣ RDS (Route)
🪧 이정표 확인
'Host가 order-api면
order-api 부서로!'"] CDS["3️⃣ CDS (Cluster)
🏢 목적지 부서 규칙
'order-api 부서는
ROUND_ROBIN으로 분배!'"] EDS["4️⃣ EDS (Endpoint)
📍 실제 파드 IP 목록
'10.244.1.2:8080
10.244.2.2:8080'"] SDS["5️⃣ SDS (Secret)
🪪 전자 신분증(mTLS)
'SPIFFE X.509 인증서'"] LDS --> RDS --> CDS --> EDS SDS -. "암호화 적용" .-> EDS end EDS == "진짜 파드 IP로 직송!" ==> POD1["order-api 파드 1
10.244.1.2:8080"] EDS == "진짜 파드 IP로 직송!" ==> POD2["order-api 파드 2
10.244.2.2:8080"]

이 5가지를 우체국에서 소포를 받아 배달하는 과정에 비유하면 평생 안 헷갈립니다!

순서 약어 원래 이름 우체국 비유 Envoy 안에서 하는 구체적인 역할 실습에서 확인할 명령어
1 LDS Listener Discovery Service 접수 창구(문) 몇 번 포트(창구)를 열고 패킷을 받을지 정합니다.
• 0.0.0.0:15001 (virtualOutbound): 내 파드에서 밖으로 나가는 모든 요청을 받는 총괄 창구
• 0.0.0.0:15006 (virtualInbound): 밖에서 내 파드로 들어오는 모든 요청을 받는 총괄 창구
istioctl proxy-config listener
2 RDS Route Discovery Service 안내 이정표 창구로 들어온 HTTP 요청의 도메인 이름(Host: order-api) 이나 경로(/pay) 를 보고, 어느 목적지 부서(Cluster)로 보낼지 매칭합니다. istioctl proxy-config route
3 CDS Cluster Discovery Service 목적지 부서 명부와 배달 규칙 목적지 부서(예: order-api 부서)로 보낼 때 어떤 방식으로 나눌지(라운드로빈 등), 타임아웃이나 서킷 브레이커 한도는 얼마로 할지 정합니다. istioctl proxy-config cluster
4 EDS Endpoint Discovery Service 실제 직원 자리 주소록 (진짜 파드 IP) 그 부서에 지금 실제로 출근해서 살아 있는 파드들의 진짜 IP와 포트(10.244.1.2:8080, 10.244.2.2:8080) 목록입니다. istioctl proxy-config endpoint
5 SDS Secret Discovery Service 전자 신분증과 암호 열쇠 상대방 파드의 Envoy와 안전하게 암호화 통신(mTLS)을 할 때 쓰는 X.509 인증서와 개인키를 실시간으로 발급·갱신합니다. istioctl proxy-config secret

5-3. 초보자가 꼭 짚고 넘어가야 할 2가지 핵심 디테일!

① Envoy가 부르는 Cluster(클러스터)는 쿠버네티스 클러스터가 아닙니다!

쿠버네티스를 배우고 온 분들이 가장 많이 헷갈리는 단어입니다.

② 왜 Envoy는 쿠버네티스 Service의 대표 IP(10.96.x.x)로 안 보내고 EDS의 파드 IP(10.244.x.x)로 직접 쏠까요?

3절에서 배운 내용을 떠올려 보세요!
만약 Envoy가 요청을 보낼 때 쿠버네티스 Service의 고정 대표 IP(ClusterIP, 예: 10.96.236.120)로 패킷을 보내버리면, 노드의 kube-proxy(L4) 가 또 끼어들어서 한쪽 파드로만 패킷을 몰아주는 연결 고착화(Connection Pinning)가 일어납니다.
그래서 Envoy는 쿠버네티스 Service의 대표 IP를 통해 RDS/CDS에서 어떤 서비스인지 확인만 한 뒤, 실제로 패킷을 밖으로 쏠 때는 kube-proxy를 완전히 건너뛰고 EDS 주소록에 적힌 살아 있는 파드들의 진짜 IP(10.244.1.2:8080, 10.244.2.2:8080)로 직접 배달합니다!


5-4. Envoy 의 목적지 이름표 읽는 법 (실습 예습)

잠시 뒤 실습에서 istioctl proxy-config cluster 나 endpoint 명령어를 치면 아래와 같이 파이프(|) 기호가 들어간 긴 이름이 나옵니다. 읽는 법을 미리 알면 한눈에 보입니다!

outbound | 80 |          | order-api.part1-demo.svc.cluster.local
────────   ──   ────────   ──────────────────────────────────────
 ① 방향    ②포트 ③ 서브셋     ④ 목적지 서비스의 전체 도메인 이름 (FQDN)
  1. outbound (방향): 내 파드에서 바깥(다른 서비스)으로 나가는 목적지라는 뜻입니다. (반대로 밖에서 내 파드 안의 컨테이너로 들어오는 길은 inbound라고 적힙니다.)
  2. 80 (포트): 쿠버네티스 Service가 열어 둔 포트 번호입니다.
  3. (비어 있음) (서브셋, Subset): 같은 서비스 안에서도 v1(구버전) 파드 묶음과 v2(신버전) 파드 묶음을 따로 나눌 때 쓰는 칸입니다. (5주차 카나리 배포 실습에서 이 빈칸에 v1, v2가 채워지는 것을 직접 봅니다!)
  4. order-api.part1-demo.svc.cluster.local (FQDN): <서비스이름>.<네임스페이스>.svc.cluster.local 형식의 쿠버네티스 내부 전체 도메인 주소입니다.
용어 정리

  • xDS (Discovery Services): Envoy 프록시가 컨트롤 플레인(istiod)으로부터 리스너(LDS), 라우트(RDS), 클러스터(CDS), 엔드포인트(EDS), 인증서(SDS) 설정을 재시작 없이 실시간으로 받아오는 표준 프로토콜 모음입니다.
  • ADS (Aggregated Discovery Service): 5가지 xDS 정보를 각각 따로 요청하면 순서가 꼬일 수 있으므로(예: 부서 CDS가 아직 안 만들어졌는데 직원 IP EDS가 먼저 도착하는 등), 단 하나의 gRPC 스트리밍 통로로 묶어서 순서대로 안전하게 전달하는 방식입니다.
  • FQDN (Fully Qualified Domain Name, 전체 도메인 이름): 줄임말(order-api)이 아니라 소속 부서와 끝자리까지 모두 적은 공식 전체 주소(order-api.part1-demo.svc.cluster.local)입니다.
  • SPIFFE (Secure Production Identity Framework for Everyone): 파드의 IP 주소는 수시로 바뀌므로 IP로 신원을 확인할 수 없습니다. 대신 spiffe://cluster.local/ns/<네임스페이스>/sa/<서비스어카운트> 처럼 "어느 네임스페이스의 어떤 권한으로 뜬 파드인가"를 X.509 인증서 안에 새겨 넣어 증명하는 클라우드 네이티브 표준 신분증 규격입니다(4주차에 아주 깊게 다룹니다).


6. 서비스 메시의 3세대 진화와 LGTM 풀스택 관측 로드맵

오늘 다룬 1세대 사이드카(Sidecar) 메시는 서비스 메시의 모든 원리가 담긴 출발점입니다. 하지만 모든 파드마다 Envoy 컨테이너를 하나씩 다 끼워 넣다 보니, 파드가 1,000개면 프록시도 1,000개가 떠서 메모리를 많이 먹고 파드마다 iptables 설정이 복잡해지는 단점이 있었습니다.

그래서 이 스터디는 1세대에서 멈추지 않고 2세대 Ambient Mesh(사이드카 없는 메시), 3세대 하이브리드 VM 메시, PostgreSQL 내부 아키텍처(MVCC·Lock), 그리고 Grafana Alloy + LGTM(Loki · Grafana · Tempo · Mimir/VictoriaMetrics) + Kiali 풀스택 관측까지 총 12개 파트로 나아갑니다.

단계 파트 아키텍처 영역 핵심 내용 요약
1세대 Part 01 ~ 02 Sidecar Mesh 파드마다 Envoy 프록시(istio-proxy) 컨테이너를 함께 넣어(2/2) xDS와 커널 iptables NAT(15006/15001) 해부
2세대 Part 03 ~ 05 Ambient Mesh (Sidecarless) 파드 안에는 사이드카를 넣지 않고(1/1), 노드별 L4 프록시(ztunnel) + 선택적 L7 프록시(Waypoint) + SPIFFE 보안 + 서킷 브레이커
3세대 Part 06 ~ 07 Hybrid VM & PostgreSQL Internals 외부 Linux VM 서버(WorkloadEntry) 메시 편입 + PostgreSQL MVCC·Row Lock(pg_locks)·pg_stat_statements + Terraform IaC
관측 Part 08 ~ 12 LGTM + Alloy + Kiali & Chaos Alloy 수집 파이프라인 + Mimir/VictoriaMetrics(메트릭) + Loki(로그/LogQL) + Tempo(분산 트레이싱) + Grafana/Kiali 상관분석 & 카오스 장애 분리

핵심 요약

Part 01 핵심 정리
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
① 왜 메시인가?   Gateway API 는 정문(남북 트래픽)만 담당.
                클러스터 내부 파드끼리의 통신(동서 트래픽, 80~90%)과 다중 언어(폴리글랏) 문제를
                앱 코드 수정 없이 해결하기 위해 탄생!
② 사이드카      원래 앱 컨테이너 옆(같은 Pod 안)에 붙는 전담 비서 컨테이너(Envoy 프록시).
                내 앱 앞에서는 포워드 프록시(발신 비서), 남의 요청 앞에서는 리버스 프록시(수신 비서).
③ L4 vs L7      기본 kube-proxy(L4)는 첫 TCP 연결(SYN) 때만 파드를 골라 'Keep-Alive / gRPC' 시
                한 파드로만 요청이 100% 쏠림(Connection Pinning).
                Envoy(L7)는 TCP 연결을 유지한 채 그 안의 HTTP 요청을 한 장씩 뜯어 골고루 분배!
④ 두 기둥       데이터 플레인(Envoy: 현장에서 실제 패킷 전달) vs 컨트롤 플레인(istiod: 작전 지시 + 인증서).
                istiod 가 잠깐 죽어도 데이터 플레인의 기존 고객 트래픽은 절대 끊기지 않음!
⑤ Envoy xDS     재시작 없이 실시간으로 설정을 받는 5대 규격:
                LDS(창구: 15001/15006) → RDS(이정표: Host/경로) → CDS(부서 규칙: ROUND_ROBIN)
                → EDS(진짜 파드 IP 직송!) + SDS(SPIFFE 전자 신분증)

참고 자료

공식 문서:


네비게이션

이전 목록 다음
◀ 00. 사전 준비 목록으로 Part 01 실습 ▶